Skip to content

feat(loop): spend ceilings — stop-after self-disarm, per-issue attempt budget, daily ceiling (#95) - #127

Merged
robercano-ghbot merged 2 commits into
mainfrom
feat/issue-95-loop-spend-ceilings
Jul 16, 2026
Merged

feat(loop): spend ceilings — stop-after self-disarm, per-issue attempt budget, daily ceiling (#95)#127
robercano-ghbot merged 2 commits into
mainfrom
feat/issue-95-loop-spend-ceilings

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

Closes #95

Adds cost/spend rails to the autonomous PR loop so an armed loop cannot run unattended forever and a stuck issue cannot ping-pong feedback cycles indefinitely.

What

  1. stop-after self-disarm — arming records an expiry (.claude/state/loop-arming.json, default +7d via budget.stop_after_days). loop-tick.sh STEP 0 pre-flight emits action=none reason=expired once expired and posts a one-time "loop disarmed, re-arm" notice. Re-arming clears the marker.
  2. Per-issue attempt budget — per-issue advance/feedback counts tracked in .claude/state/loop-issue-attempts.json (budget.per_issue_attempts, default 5). On breach: refuse advance, label the PR needs-human and comment once.
  3. Daily activity ceiling — per-day action count in .claude/state/loop-daily-ceiling.json (budget.daily_action_ceiling, default 50). On breach: halt (action=none reason=daily-ceiling), file/refresh a single backlog-labeled "budget exceeded" issue (never planned), auto-reset next calendar day.
  4. Cockpit surfacingcockpit.sh loop-health panel shows stop-after countdown, today's actions vs ceiling, and per-issue attempt counts.

All three limits configurable via .claude/self/gates.jsonbudget with sane defaults; documented in docs/TOKEN_BUDGET.md.

Design notes

  • All ceiling checks are pre-flight in loop-tick.sh (before the spawn-lock side effect); the daemon never re-derives the verdict (issue Harden PR-loop tick: deterministic loop-tick.sh with single verdict, in-flight detection, spawn lock #81 contract preserved).
  • Ceiling read path does no network; gh side-effects (label/comment/file-issue) fire only on an actual breach, all via bot-gh.sh.
  • Verdict-last-line stdout invariant preserved; state writes are atomic (temp+mv), JSON via node, degrade safely when state files are absent.
  • CLAUDE_TODAY test seam added for deterministic daily-reset tests (defaults to date -u).

Gates

GATES_FILE=.claude/self/gates.json build / lint / test all green, including smoke-fanout.sh. New loop-ceilings.test.sh (45 checks) plus cockpit panel tests.

Review

Correctness (opus) and tests (sonnet) lenses both APPROVE (consensus all). Tests reviewer mutation-tested the label and reuse-branch assertions to confirm they are load-bearing.

🤖 Generated with Claude Code

robercano and others added 2 commits July 16, 2026 09:20
…, daily ceiling (#95)

Bounds the autonomous PR loop's aggregate spend, mirroring gh-aw's cost-management
design: an armed loop now self-disarms after budget.stop_after_days (default 7,
recorded by arm-loop.sh in .claude/state/loop-arming.json, with a lazy fallback
init for loops armed before this feature existed); an issue ping-ponging through
budget.per_issue_attempts (default 5) advance/feedback dispatches without landing
gets refused and labeled needs-human instead of retried forever; and a
budget.daily_action_ceiling (default 50) halts new dispatches for the rest of the
UTC day, filing/refreshing a single tracking issue, then resumes automatically at
midnight. All three are pre-flight checks in loop-tick.sh's STEP 0, before the
verdict decision, so a breach skips the spawn-lock side effect entirely; every gh
side effect (notify/label/comment/file-issue) is once-guarded and best-effort. The
cockpit's Loop health panel now surfaces stop-after countdown, today's action count
vs the daily ceiling, and per-issue attempt counts vs the budget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…use path

TESTS reviewer rejected two blocking coverage gaps on the spend-ceilings
change (#95):

1. loop-ceilings.test.sh's stubbed-gh assertions for the ceiling notify
   path only checked `grep -q "^issue create"`, never the `--label backlog`
   argument. A silent regression to `--label planned` (or a dropped label)
   would go undetected, and since loop-census.sh treats `planned`-labeled
   issues as work, that regression would make the loop treat its own
   budget-exceeded notice as a new work item. Scenarios 2 and 8 now assert
   the FULL expected `issue create` call including `--label backlog`, plus
   a negative assertion that `--label planned` is never emitted.

2. budget_notify_issue()'s reuse path (loop-tick.sh:313-318 -- `gh issue
   view` an existing tracked issue and comment if OPEN, or file fresh if
   CLOSED) had zero coverage. New scenario 10 drives three ticks against one
   fixture: first breach files a tracking issue, second breach (issue OPEN)
   comments on it instead of duplicating, third breach (issue CLOSED) files
   a fresh one. The fake bot-gh.sh's `issue view` reply is now configurable
   via FAKE_ISSUE_STATE.

Also added a CLAUDE_TODAY override in loop-tick.sh (mirrors cockpit.sh's
COCKPIT_NOW pattern), defaulting to `date -u +%Y-%m-%d` when unset, and wired
it through the date-sensitive test scenarios (7, 8, 9, 10) to close a narrow
UTC-midnight flake window where the test and the script could independently
compute a different calendar date.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@robercano-ghbot
robercano-ghbot merged commit fb50c51 into main Jul 16, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-95-loop-spend-ceilings branch July 16, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loop: spend ceilings — per-tick caps, daily budget with auto-halt, stop-after self-disarm

2 participants